using System;
using System.Drawing;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace JelleDruyts.Windows
{
    /// <summary>
    /// Provides services to manipulate images.
    /// </summary>
    public static class ImageManipulator
    {
        #region Constants

        private const BitmapScalingMode DefaultScalingMode = BitmapScalingMode.HighQuality;
        private const int DefaultQualityLevel = 90;
        private const double DefaultDpi = 96;

        #endregion

        #region GetDimensions

        /// <summary>
        /// Gets the dimensions of an image.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <returns>The dimensions of the image.</returns>
        public static Size GetDimensions(string fileName)
        {
            using (var stream = File.OpenRead(fileName))
            {
                var frame = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.None);
                return new Size(frame.PixelWidth, frame.PixelHeight);
            }
        }

        #endregion

        #region GetMetadata

        /// <summary>
        /// Gets the metadata of an image.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <returns>The metadata of the image.</returns>
        public static BitmapMetadata GetMetadata(string fileName)
        {
            using (var stream = File.OpenRead(fileName))
            {
                var frame = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.Default);
                return (BitmapMetadata)frame.Metadata;
            }
        }

        #endregion

        #region ResizeToMaximumSize

        /// <summary>
        /// Resizes an image to a specified maximum file size.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="maximumSizeBytes">The maximum size (in bytes) of the resized image.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public static Stream ResizeToMaximumSize(string fileName, long maximumSizeBytes)
        {
            var originalSize = new FileInfo(fileName).Length;
            if (originalSize <= maximumSizeBytes)
            {
                return File.OpenRead(fileName);
            }
            else
            {
                // As a first guess of the percentage for resizing the side, assume the square root of the target file size
                // divided by the original file size (because doubling the sides would quadruple the file size not taking
                // compression into account.
                // This algorithm has room for improvement, e.g. don't just stop as soon as it's smaller than the target
                // but keep getting closer to the maximum until a certain delta is reached.
                // Optionally also build in a safety net to only perform a specified number of attempts at resizing.
                var percentage = Math.Sqrt((double)maximumSizeBytes / originalSize);
                long? encodedSize = null;
                Stream result = null;
                while (encodedSize == null || encodedSize > maximumSizeBytes)
                {
                    if (result != null)
                    {
                        // Dispose any previous attempt.
                        result.Dispose();
                    }
                    result = Resize(fileName, null, null, percentage, null, null, null, null, out encodedSize);
                    if (encodedSize == null)
                    {
                        // If the percentage was close enough to 1 so that the end result was the same number of pixels,
                        // the image has not been resized.
                        result = File.OpenRead(fileName);
                    }
                    // Steadily decrease the percentage until the desired size is reached.
                    percentage -= 0.02;
                    if (percentage <= 0)
                    {
                        throw new ArgumentException("The image could not be resized to the specified maximum number of bytes.");
                    }
                }
                return result;
            }
        }

        #endregion

        #region ResizeToPercentage

        /// <summary>
        /// Resizes an image with a specified percentage.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="percentage">The percentage to resize the image.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        public static Stream ResizeToPercentage(string fileName, double percentage)
        {
            return Resize(fileName, null, null, percentage, null, null, null, null);
        }

        /// <summary>
        /// Resizes an image with a specified percentage.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="percentage">The percentage to resize the image.</param>
        /// <param name="scalingMode">The scaling mode to use for the resized image.</param>
        /// <param name="encoder">The encoder to use for the resized image.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        public static Stream ResizeToPercentage(string fileName, double percentage, BitmapScalingMode scalingMode, BitmapEncoder encoder)
        {
            return Resize(fileName, null, null, percentage, null, scalingMode, encoder, null);
        }

        #endregion

        #region ResizeToLongestSide

        /// <summary>
        /// Resizes an image so that the longest side is the specified size.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="longestSide">The size of the longest side of the resized image.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        public static Stream ResizeToLongestSide(string fileName, int longestSide)
        {
            return Resize(fileName, null, null, null, longestSide, null, null, null);
        }

        /// <summary>
        /// Resizes an image so that the longest side is the specified size.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="longestSide">The size of the longest side of the resized image.</param>
        /// <param name="scalingMode">The scaling mode to use for the resized image.</param>
        /// <param name="encoder">The encoder to use for the resized image.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        public static Stream ResizeToLongestSide(string fileName, int longestSide, BitmapScalingMode scalingMode, BitmapEncoder encoder)
        {
            return Resize(fileName, null, null, null, longestSide, scalingMode, encoder, null);
        }

        #endregion

        #region ResizeToDimensions

        /// <summary>
        /// Resizes an image to a specified dimension.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="width">The new width of the resized image, or <see langword="null"/> to resize proportionally to the new <paramref name="height"/>.</param>
        /// <param name="height">The new height of the resized image, or <see langword="null"/> to resize proportionally to the new <paramref name="width"/>.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        public static Stream ResizeToDimensions(string fileName, int? width, int? height)
        {
            return Resize(fileName, width, height, null, null, null, null, null);
        }

        /// <summary>
        /// Resizes an image to a specified dimension.
        /// </summary>
        /// <param name="fileName">The file name of the image.</param>
        /// <param name="width">The new width of the resized image, or <see langword="null"/> to resize proportionally to the new <paramref name="height"/>.</param>
        /// <param name="height">The new height of the resized image, or <see langword="null"/> to resize proportionally to the new <paramref name="width"/>.</param>
        /// <returns>A stream that contains the resized image, or the original image if it did not need to be resized.</returns>
        /// <param name="scalingMode">The scaling mode to use for the resized image.</param>
        /// <param name="encoder">The encoder to use for the resized image.</param>
        public static Stream ResizeToDimensions(string fileName, int? width, int? height, BitmapScalingMode scalingMode, BitmapEncoder encoder)
        {
            return Resize(fileName, width, height, null, null, scalingMode, encoder, null);
        }

        #endregion

        #region Helper Methods

        private static Stream Resize(string fileName, int? width, int? height, double? percentage, int? longestSide, BitmapScalingMode? scalingMode, BitmapEncoder encoder, double? dpi)
        {
            long? encodedSize;
            var stream = Resize(fileName, width, height, percentage, longestSide, scalingMode, encoder, dpi, out encodedSize);
            return stream;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        private static Stream Resize(string fileName, int? width, int? height, double? percentage, int? longestSide, BitmapScalingMode? scalingMode, BitmapEncoder encoder, double? dpi, out long? encodedSize)
        {
            if (scalingMode == null)
            {
                scalingMode = DefaultScalingMode;
            }
            if (encoder == null)
            {
                encoder = new JpegBitmapEncoder { QualityLevel = DefaultQualityLevel };
            }
            if (dpi == null)
            {
                dpi = DefaultDpi;
            }
            encodedSize = null;
            if (width == null && height == null && (percentage == null || percentage.Value == 1) && longestSide == null)
            {
                return File.OpenRead(fileName);
            }
            else
            {
                using (var originalImageStream = File.OpenRead(fileName))
                {
                    var original = BitmapFrame.Create(originalImageStream);
                    var resized = ResizeFrame(original, width, height, percentage, longestSide, scalingMode.Value, dpi.Value);
                    if (resized == null)
                    {
                        return File.OpenRead(fileName);
                    }
                    var encodedImageStream = new MemoryStream();
                    encoder.Frames.Add(resized);
                    encoder.Save(encodedImageStream);
                    encodedSize = encodedImageStream.Length;
                    encodedImageStream.Seek(0, SeekOrigin.Begin);
                    return encodedImageStream;
                }
            }
        }

        // http://weblogs.asp.net/bleroy/archive/2009/12/10/resizing-images-from-the-server-using-wpf-wic-instead-of-gdi.aspx
        private static BitmapFrame ResizeFrame(BitmapFrame image, int? width, int? height, double? percentage, int? longestSide, BitmapScalingMode scalingMode, double dpi)
        {
            var finalWidth = 0;
            var finalHeight = 0;
            if (percentage != null)
            {
                finalWidth = (int)(image.PixelWidth * percentage.Value);
                finalHeight = (int)(image.PixelHeight * percentage.Value);
            }
            else if (longestSide != null)
            {
                if (image.PixelWidth > image.PixelHeight)
                {
                    // Landscape mode.
                    finalWidth = longestSide.Value;
                    finalHeight = (image.PixelHeight * finalWidth) / image.PixelWidth;
                }
                else
                {
                    // Portrait mode.
                    finalHeight = longestSide.Value;
                    finalWidth = (image.PixelWidth * finalHeight) / image.PixelHeight;
                }
            }
            else if (width == null)
            {
                finalHeight = height.Value;
                finalWidth = (image.PixelWidth * finalHeight) / image.PixelHeight;
            }
            else if (height == null)
            {
                finalWidth = width.Value;
                finalHeight = (image.PixelHeight * finalWidth) / image.PixelWidth;
            }
            else
            {
                finalHeight = height.Value;
                finalWidth = width.Value;
            }
            if (finalHeight == image.PixelHeight && finalWidth == image.PixelWidth)
            {
                return null;
            }
            var group = new DrawingGroup();
            RenderOptions.SetBitmapScalingMode(group, scalingMode);
            group.Children.Add(new ImageDrawing(image, new System.Windows.Rect(0, 0, finalWidth, finalHeight)));
            var targetVisual = new DrawingVisual();
            RenderTargetBitmap target;
            using (var targetContext = targetVisual.RenderOpen())
            {
                targetContext.DrawDrawing(group);
                target = new RenderTargetBitmap(finalWidth, finalHeight, dpi, dpi, PixelFormats.Default);
            }
            target.Render(targetVisual);
            return BitmapFrame.Create(target, image.Thumbnail, image.Metadata as BitmapMetadata, image.ColorContexts);
        }

        #endregion
    }
}